{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# RAGAS 를 활용한 평가\n",
    "\n",
    "**참고**\n",
    "- RAGAS: https://docs.ragas.io/en/latest/getstarted/evaluation.html"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "아래의 주석을 해제한 후 실행하여 패키지를 설치 후 진행해주세요"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install -qU faiss-cpu ragas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH16-Evaluations\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 저장한 CSV 파일로부터 로드\n",
    "\n",
    "- `data/ragas_synthetic_dataset.csv` 파일을 로드합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "df = pd.read_csv(\"data/ragas_synthetic_dataset.csv\")\n",
    "df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from datasets import Dataset\n",
    "\n",
    "test_dataset = Dataset.from_pandas(df)\n",
    "test_dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ast\n",
    "\n",
    "\n",
    "# contexts 컬럼의 문자열을 리스트로 변환\n",
    "def convert_to_list(example):\n",
    "    contexts = ast.literal_eval(example[\"contexts\"])\n",
    "    return {\"contexts\": contexts}\n",
    "\n",
    "\n",
    "test_dataset = test_dataset.map(convert_to_list)\n",
    "print(test_dataset)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "test_dataset[1][\"contexts\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "from langchain_community.document_loaders import PyMuPDFLoader\n",
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "from langchain_core.prompts import PromptTemplate\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
    "\n",
    "# 단계 1: 문서 로드(Load Documents)\n",
    "loader = PyMuPDFLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "docs = loader.load()\n",
    "\n",
    "# 단계 2: 문서 분할(Split Documents)\n",
    "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)\n",
    "split_documents = text_splitter.split_documents(docs)\n",
    "\n",
    "# 단계 3: 임베딩(Embedding) 생성\n",
    "embeddings = OpenAIEmbeddings()\n",
    "\n",
    "# 단계 4: DB 생성(Create DB) 및 저장\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)\n",
    "\n",
    "# 단계 5: 검색기(Retriever) 생성\n",
    "# 문서에 포함되어 있는 정보를 검색하고 생성합니다.\n",
    "retriever = vectorstore.as_retriever()\n",
    "\n",
    "# 단계 6: 프롬프트 생성(Create Prompt)\n",
    "# 프롬프트를 생성합니다.\n",
    "prompt = PromptTemplate.from_template(\n",
    "    \"\"\"You are an assistant for question-answering tasks. \n",
    "Use the following pieces of retrieved context to answer the question. \n",
    "If you don't know the answer, just say that you don't know. \n",
    "\n",
    "#Context: \n",
    "{context}\n",
    "\n",
    "#Question:\n",
    "{question}\n",
    "\n",
    "#Answer:\"\"\"\n",
    ")\n",
    "\n",
    "# 단계 7: 언어모델(LLM) 생성\n",
    "# 모델(LLM) 을 생성합니다.\n",
    "llm = ChatOpenAI(model_name=\"gpt-4o\", temperature=0)\n",
    "\n",
    "# 단계 8: 체인(Chain) 생성\n",
    "chain = (\n",
    "    {\"context\": retriever, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "배치 데이터셋을 생성합니다. 배치 데이터셋은 다량의 질문을 한 번에 처리할 때 용이합니다.\n",
    "\n",
    "- 배치: https://wikidocs.net/233345"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "batch_dataset = [question for question in test_dataset[\"question\"]]\n",
    "batch_dataset[:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`batch()` 를 호출하여 배치 데이터셋에 대한 답변을 받습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "answer = chain.batch(batch_dataset)\n",
    "answer[:3]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "LLM 이 생성한 답변을 'answer' 컬럼에 저장합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 'answer' 컬럼 덮어쓰기 또는 추가\n",
    "if \"answer\" in test_dataset.column_names:\n",
    "    test_dataset = test_dataset.remove_columns([\"answer\"]).add_column(\"answer\", answer)\n",
    "else:\n",
    "    test_dataset = test_dataset.add_column(\"answer\", answer)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 답변 평가"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Context Recall\n",
    "\n",
    "**요약** \n",
    "\n",
    "- \"검색된 context가 LLM 이 생성한 답변과 얼마나 일치하는지\" 를 측정합니다.\n",
    "\n",
    "Context recall은 검색된 context가 LLM 이 생성한 답변과 얼마나 일치하는지를 측정합니다. \n",
    "\n",
    "이는 question, ground truth 및 검색된 context를 사용하여 계산되며, 값은 0에서 1 사이로, 높을수록 더 나은 성능을 나타냅니다. \n",
    "\n",
    "Ground truth 답변에서 context recall을 추정하기 위해, ground truth 답변의 각 주장이 검색된 context에 귀속될 수 있는지 분석됩니다. 이상적인 시나리오에서는 ground truth 답변의 모든 주장이 검색된 context에 귀속될 수 있어야 합니다. \n",
    "\n",
    "$$\\text{context recall} = \\frac{|\\text{GT claims that can be attributed to context}|}{|\\text{Number of claims in GT}|}$$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Context Precision\n",
    "\n",
    "**요약**\n",
    "\n",
    "- \"얼마나 관련성 있는 문서가 상위에 배치되었는가?\" 를 평가하는 지표입니다. \n",
    "\n",
    "Context Precision은 contexts 내의 ground-truth 관련 항목들이 상위 순위에 있는지를 평가하는 지표입니다. 이상적으로는 모든 관련 chunks가 상위 순위에 나타나야 합니다. 이 지표는 question, ground_truth, 그리고 contexts를 사용하여 계산되며, 0에서 1 사이의 값을 가집니다. 높은 점수일수록 더 나은 정밀도를 나타냅니다.\n",
    "\n",
    "Context Precision@K의 계산식은 다음과 같습니다.\n",
    "\n",
    "$$\\text{Context Precision@K} = \\frac{\\sum_{k=1}^{K} (\\text{Precision@k} \\times v_k)}{\\text{Total number of relevant items in the top K results}}$$\n",
    "\n",
    "여기서 Precision@k는 다음과 같이 계산됩니다.\n",
    "\n",
    "$$\\text{Precision@k} = \\frac{\\text{true positives@k}}{(\\text{true positives@k + false positives@k})}$$\n",
    "\n",
    "K는 contexts의 총 chunk 수이며, $v_k \\in \\{0, 1\\}$은 순위 k에서의 관련성 지표입니다.\n",
    "\n",
    "이 지표는 정보 검색 시스템에서 검색된 컨텍스트의 품질을 평가하는 데 사용됩니다. 관련 정보가 얼마나 정확하게 상위 순위에 배치되었는지를 측정함으로써 시스템의 성능을 판단할 수 있습니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Answer Relevancy\n",
    "\n",
    "- \"생성된 답변이 주어진 prompt에 얼마나 적절한지\" 를 평가하는 지표입니다. \n",
    "\n",
    "이 지표의 주요 특징과 계산 방법을 요약하면 다음과 같습니다.\n",
    "\n",
    "1. 목적: 생성된 답변의 관련성을 평가합니다.\n",
    "2. 점수 해석: 낮은 점수는 불완전하거나 중복 정보를 포함한 답변을, 높은 점수는 더 나은 관련성을 나타냅니다.\n",
    "3. 계산에 사용되는 요소: question, context, answer\n",
    "\n",
    "Answer Relevancy의 계산 방법:\n",
    "- 원래 question과 answer를 기반으로 생성된 합성 질문들 간의 평균 코사인 유사도로 정의됩니다.\n",
    "- 수식:\n",
    "\n",
    "$$\\text{answer relevancy} = \\frac{1}{N} \\sum_{i=1}^N \\cos(E_{g_i}, E_o)$$\n",
    "\n",
    "또는\n",
    "\n",
    "$$\\text{answer relevancy} = \\frac{1}{N} \\sum_{i=1}^N \\frac{E_{g_i} \\cdot E_o}{\\|E_{g_i}\\| \\|E_o\\|}$$\n",
    "\n",
    "여기서:\n",
    "- $E_{g_i}$는 생성된 질문 $i$의 임베딩\n",
    "- $E_o$는 원래 질문의 임베딩\n",
    "- $N$은 생성된 질문의 수 (기본값 3)\n",
    "\n",
    "주의사항:\n",
    "- 실제로는 점수가 대부분 0과 1 사이에 있지만, 코사인 유사도의 특성상 수학적으로 -1에서 1 사이의 값을 가질 수 있습니다.\n",
    "\n",
    "이 지표는 질문-답변 시스템의 성능을 평가하는 데 유용하며, 특히 생성된 답변이 원래 질문의 의도를 얼마나 잘 반영하는지를 측정합니다."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Faithfulness\n",
    "\n",
    "- \"생성된 답변의 사실적 일관성을 주어진 컨텍스트와 비교하여 측정\" 하는 지표입니다. \n",
    "\n",
    "주요 특징은 다음과 같습니다.\n",
    "\n",
    "1. 목적: 답변의 사실적 일관성을 컨텍스트와 비교하여 평가합니다.\n",
    "2. 계산 요소: 답변과 검색된 컨텍스트를 사용합니다.\n",
    "3. 점수 범위: 0에서 1 사이로 조정되며, 높을수록 더 좋습니다.\n",
    "\n",
    "Faithfulness 점수 계산 방법:\n",
    "\n",
    "$$\\text{Faithfulness score} = \\frac{|\\text{Number of claims in the generated answer that can be inferred from given context}|}{|\\text{Total number of claims in the generated answer}|}$$\n",
    "\n",
    "계산 과정:\n",
    "1. 생성된 답변에서 주장(claims)들을 식별합니다.\n",
    "2. 각 주장을 주어진 컨텍스트와 대조 검증하여 컨텍스트에서 추론 가능한지 확인합니다.\n",
    "3. 위 수식을 사용하여 점수를 계산합니다.\n",
    "\n",
    "예시:\n",
    "- 질문: \"아인슈타인은 어디서, 언제 태어났나요?\"\n",
    "- 컨텍스트: \"알버트 아인슈타인(1879년 3월 14일 출생)은 독일 출신의 이론 물리학자로, 역사상 가장 위대하고 영향력 있는 과학자 중 한 명으로 여겨집니다.\"\n",
    "- 높은 충실도 답변: \"아인슈타인은 1879년 3월 14일 독일에서 태어났습니다.\"\n",
    "- 낮은 충실도 답변: \"아인슈타인은 1879년 3월 20일 독일에서 태어났습니다.\"\n",
    "\n",
    "이 지표는 생성된 답변이 주어진 컨텍스트에 얼마나 충실한지를 평가하는 데 유용하며, 특히 질문-답변 시스템의 정확성과 신뢰성을 측정하는 데 중요합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from ragas import evaluate\n",
    "from ragas.metrics import (\n",
    "    answer_relevancy,\n",
    "    faithfulness,\n",
    "    context_recall,\n",
    "    context_precision,\n",
    ")\n",
    "\n",
    "result = evaluate(\n",
    "    dataset=test_dataset,\n",
    "    metrics=[\n",
    "        context_precision,\n",
    "        faithfulness,\n",
    "        answer_relevancy,\n",
    "        context_recall,\n",
    "    ],\n",
    ")\n",
    "\n",
    "result"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result_df = result.to_pandas()\n",
    "result_df.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result_df.to_csv(\"data/ragas_evaluation_result.csv\", index=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "result_df.loc[:, \"context_precision\":\"context_recall\"]"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
